package io.github.melin.sqlflow.analyzer;

import com.google.common.collect.ImmutableSet;
import io.github.melin.sqlflow.metadata.QualifiedObjectName;
import io.github.melin.sqlflow.tree.Node;
import io.github.melin.sqlflow.tree.NodeLocation;
import io.github.melin.sqlflow.tree.QualifiedName;
import io.github.melin.sqlflow.tree.expression.Identifier;
import io.github.melin.sqlflow.type.Type;

import java.util.*;
import java.util.stream.Collectors;

import static io.github.melin.sqlflow.analyzer.SemanticExceptions.semanticException;
import static java.util.Objects.requireNonNull;

public class Field {
    private NodeLocation location;
    private final Optional<QualifiedObjectName> originTable;
    private final Optional<String> originColumnName;
    private final Optional<QualifiedName> relationAlias;
    private final Optional<String> name;
    private Type type;
    private final boolean aliased;

    public static Field newUnqualified(String name) {
        requireNonNull(name, "name is null");

        return new Field( Optional.empty(), Optional.of(name), Optional.empty(), Optional.empty(), false);
    }

    public static Field newUnqualified(Optional<String> name) {
        requireNonNull(name, "name is null");

        return new Field(Optional.empty(), name, Optional.empty(), Optional.empty(), false);
    }

    public static Field newUnqualified(Optional<String> name, Optional<QualifiedObjectName> originTable, Optional<String> originColumn, boolean aliased) {
        requireNonNull(name, "name is null");
        requireNonNull(originTable, "originTable is null");

        return new Field(Optional.empty(), name, originTable, originColumn, aliased);
    }

    public static Field newQualified(QualifiedName relationAlias, Optional<String> name, Optional<QualifiedObjectName> originTable, Optional<String> originColumn, boolean aliased) {
        requireNonNull(relationAlias, "relationAlias is null");
        requireNonNull(name, "name is null");
        requireNonNull(originTable, "originTable is null");

        return new Field(Optional.of(relationAlias), name, originTable, originColumn, aliased);
    }

    public Field(Optional<QualifiedName> relationAlias, Optional<String> name, Optional<QualifiedObjectName> originTable, Optional<String> originColumnName, boolean aliased) {
        requireNonNull(relationAlias, "relationAlias is null");
        requireNonNull(name, "name is null");
        requireNonNull(originTable, "originTable is null");
        requireNonNull(originColumnName, "originColumnName is null");

        this.relationAlias = relationAlias;
        this.name = name;
        this.originTable = originTable;
        this.originColumnName = originColumnName;
        this.aliased = aliased;
    }

    public NodeLocation getLocation() {
        return location;
    }

    public void setLocation(NodeLocation location) {
        this.location = location;
    }

    public Optional<QualifiedObjectName> getOriginTable() {
        return originTable;
    }

    public Optional<String> getOriginColumnName() {
        return originColumnName;
    }

    public Optional<QualifiedName> getRelationAlias() {
        return relationAlias;
    }

    public Optional<String> getName() {
        return name;
    }

    public Type getType() {
        return type;
    }

    public boolean isAliased() {
        return aliased;
    }

    public boolean matchesPrefix(Optional<QualifiedName> prefix) {
        return !prefix.isPresent() || relationAlias.isPresent() && relationAlias.get().hasSuffix(prefix.get());
    }

    /*
      Namespaces can have names such as "x", "x.y" or "" if there's no name
      Name to resolve can have names like "a", "x.a", "x.y.a"

      namespace  name     possible match
       ""         "a"           y
       "x"        "a"           y
       "x.y"      "a"           y

       ""         "x.a"         n
       "x"        "x.a"         y
       "x.y"      "x.a"         n

       ""         "x.y.a"       n
       "x"        "x.y.a"       n
       "x.y"      "x.y.a"       n

       ""         "y.a"         n
       "x"        "y.a"         n
       "x.y"      "y.a"         y
     */
    public boolean canResolve(QualifiedName name, boolean caseSensitive) {
        if (!this.name.isPresent()) {
            return false;
        }

        // TODO: need to know whether the qualified name and the name of this field were quoted
        if (caseSensitive) {
            return matchesPrefix(name.getPrefix()) && this.name.get().equals(name.getSuffix());
        } else {
            return matchesPrefix(name.getPrefix()) && this.name.get().equalsIgnoreCase(name.getSuffix());
        }
    }

    /**
     * 验证并匹配源字段和目标字段
     * @param sourceFields 源字段列表
     * @param targetFields 目标字段列表
     * @param node 当前处理的节点（用于错误报告）
     * @param strict 是否严格匹配（名称必须完全相同）
     * @return 匹配后的字段映射
     */
    public static Map<String, Field> matchFields(List<Field> sourceFields,
                                                 List<String> targetFields,
                                                 Node node,
                                                 boolean strict) {
        // 创建源字段名到Field的映射
        Map<String, Field> fieldsByName = sourceFields.stream()
                .collect(Collectors.toMap(
                        field -> field.getName().orElse(""),
                        field -> field,
                        (existing, replacement) -> existing  // 如果有重复，保留第一个
                ));

        // 验证所有目标字段都能找到对应的源字段
        Map<String, Field> resultMapping = new LinkedHashMap<>();
        for (String targetField : targetFields) {
            Field sourceField = fieldsByName.get(targetField);
            if (sourceField == null && strict) {
                throw semanticException(node,
                        "Column '%s' not found in source fields: %s",
                        targetField,
                        String.join(", ", fieldsByName.keySet()));
            }
            resultMapping.put(targetField, sourceField);
        }

        return resultMapping;
    }

    /**
     * 验证字段别名的有效性
     * @param columnAliases 列别名列表
     * @param relationType 关系类型
     * @param node 当前处理的节点
     */
    public static void validateColumnAliases(List<Identifier> columnAliases,
                                             RelationType relationType,
                                             Node node) {
        // 验证别名数量与字段数量匹配
        if (columnAliases.size() != relationType.getVisibleFieldCount()) {
            throw semanticException(node,
                    "Column alias list has %s entries but query has %s columns",
                    columnAliases.size(), relationType.getVisibleFieldCount());
        }

        // 验证别名唯一性
        Set<String> uniqueAliases = new HashSet<>();
        for (Identifier alias : columnAliases) {
            if (!uniqueAliases.add(alias.getValue())) {
                throw semanticException(node,
                        "Duplicate column alias: %s",
                        alias.getValue());
            }
        }
    }

    /**
     * 创建输出列，保持字段顺序
     * @param targetFields 目标字段列表
     * @param fieldMapping 字段映射关系
     * @param analysis 分析上下文
     * @return 输出列列表
     */
    public static List<OutputColumn> createOrderedOutputColumns(
            List<String> targetFields,
            Map<String, Field> fieldMapping,
            Analysis analysis) {
        return targetFields.stream()
                .map(targetField -> {
                    Field sourceField = fieldMapping.get(targetField);
                    return new OutputColumn(
                            targetField,
                            sourceField != null ? analysis.getSourceColumns(sourceField) : ImmutableSet.of()
                    );
                })
                .collect(Collectors.toList());
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();
        if (relationAlias.isPresent()) {
            result.append(relationAlias.get())
                    .append(".");
        }

        result.append(name.orElse("<anonymous>"))
                .append(":")
                .append(type);

        return result.toString();
    }
}
